iT邦幫忙

2026 iThome 鐵人賽

0

一、今日目標

延續 Day 23「ASP.NET Core 連接 Oracle」,今天開始實作智慧醫院管理平台的第一個正式資料查詢功能:醫師查詢 API。

前一天已經成功讓 ASP.NET Core 透過 OracleConnection 連接 Oracle Database,並執行:

SELECT 1 FROM DUAL;

也確認 API 能夠成功回傳 JSON。

不過,當時只是確認資料庫連線正常,尚未建立正式的業務資料表,也沒有真正讀取醫院相關資料。

因此,Day 24 希望從資料庫建立開始,逐步完成醫師查詢功能,讓 ASP.NET Core 能夠從 Oracle Database 取得醫師資料,而不是直接回傳寫死的 Demo JSON。

今日主要目標

  • 建立醫師查詢需要的 Oracle 資料表。
  • 學習 CREATE TABLE、PRIMARY KEY、FOREIGN KEY 與 CHECK。
  • 建立少量虛構醫師資料,作為 API 測試用途。
  • 使用 JOIN 與 LEFT JOIN 查詢多張資料表。
  • 認識 Controller、Repository 與 DTO 的分工。
  • 實作取得所有醫師的 API。
  • 加入醫師姓名關鍵字搜尋。
  • 加入科別篩選與組合搜尋。
  • 實作單一醫師詳細資料 API。
  • 處理 HTTP 200、400、404 與 503。
  • 測試原有 API 是否受到影響。

今天先專注於 ASP.NET Core 與 Oracle 資料庫,暫時不修改 Angular 前端。前後端的正式串接留到下一階段進行。


二、從 Day 23 的資料庫連線開始

在 Day 23,我已經完成 ASP.NET Core 與 Oracle Database 的基本連線。

當時使用智慧醫院專用的 SMARTCARE_DEV 開發帳號,透過 Oracle .NET 驅動程式建立連線。

整體流程如下:

ASP.NET Core Web API
        ↓
OracleConnection
        ↓
Oracle Database
        ↓
SELECT 1 FROM DUAL
        ↓
取得查詢結果 1
        ↓
回傳 JSON

當時的 Oracle 診斷 API 成功回傳:

{
  "status": "ok",
  "value": 1
}

這證明 ASP.NET Core 已經可以與 Oracle Database 溝通。

不過,這個 SQL 並沒有查詢任何智慧醫院的實際業務資料。

因此,今天開始將 Day 22 規劃的資料庫設計,實際建立在 Oracle 中,並讓 Web API 查詢這些資料。

這次決定先從醫師查詢開始,是因為前端已經有醫師列表、科別篩選與醫師詳細資訊等畫面,功能範圍相對明確,也適合作為第一次資料庫 API 實作。


三、確認 Day 22 的資料庫設計

Day 22 已經規劃智慧醫院預約系統需要的七張資料表:

資料表 用途
Users 會員帳號
Patients 就診者
Departments 科別
Doctors 醫師資料
DoctorSpecialties 醫師專長
DoctorSchedules 醫師門診排班
Appointments 預約掛號紀錄

不過,今天只需要完成醫師查詢 API,因此不打算一次建立所有資料表。

經過檢查,這次需要的只有三張:

  1. Departments:保存科別資料。
  2. Doctors:保存醫師基本資訊。
  3. DoctorSpecialties:保存醫師專長。

三張表之間的關係為:

erDiagram
    DEPARTMENTS ||--o{ DOCTORS : contains
    DOCTORS ||--o{ DOCTOR_SPECIALTIES : has

    DEPARTMENTS {
        number department_id PK
        string department_name
    }

    DOCTORS {
        number doctor_id PK
        number department_id FK
        string doctor_name
        string title
        string education
        string experience
        string illustration_path
    }

    DOCTOR_SPECIALTIES {
        number doctor_id PK,FK
        string specialty_name PK
    }

其中一個科別可以有多位醫師,而一位醫師可以有多個專長。

為什麼不直接在 Doctors 裡新增好幾個專長欄位?

因為每位醫師的專長數量不一定相同。

如果將專長設計成 specialty1、specialty2、specialty3 等固定欄位,不但擴充不方便,也會讓資料結構不夠彈性。

因此,Day 22 就已經將醫師專長獨立成另一張資料表。

這次沿用原本設計,沒有另外加入 Identity 或 Sequence。


四、第一次實際設定 Oracle 建表權限

開始建立資料表前,先檢查 Oracle 的開發帳號。

Day 23 建立的 SMARTCARE_DEV,原本只有:

CREATE SESSION

這個權限可以登入資料庫,但不代表可以直接建立資料表。

透過查詢發現:

  • 尚未授予 CREATE TABLE。
  • USERS Tablespace 尚未設定空間配額。
  • 三張醫師相關資料表尚不存在。

因此,必須先完成最基本的建表權限設定。

1. 授予 CREATE TABLE

這次在 SQL Developer 使用 SYS/SYSDBA 管理員連線,執行:

GRANT CREATE TABLE TO SMARTCARE_DEV;

讓 SMARTCARE_DEV 可以在自己的 Schema 建立資料表。

2. 設定 Tablespace 配額

接著執行:

ALTER USER SMARTCARE_DEV QUOTA 10M ON USERS;

設定 SMARTCARE_DEV 在 USERS Tablespace 的使用配額為 10 MB。

這裡的 10 MB 是使用上限,不是立即分配 10 MB,也不是每張資料表各有 10 MB。

3. 為什麼不直接給 DBA 權限?

因為這次只是練習建立智慧醫院的開發資料表,沒有必要提供完整資料庫管理權限。

因此只設定需要的權限與有限空間配額。

這也是延續 Day 23 學習到的最小權限原則:讓應用程式帳號只取得完成工作所需要的權限。

完成管理員設定後,再切換回 SMARTCARE_DEV,確認 CREATE TABLE 權限與 USERS 配額正常。


五、建立第一張資料表:Departments

完成權限設定後,開始建立第一張資料表。

這次採用:

確認身分 → 檢查是否存在 → 建立一張 → 驗證一張

的方式進行。

避免一次執行大量 DDL,遇到錯誤時無法確認是哪個步驟造成的。

第一張建立的是 Departments,因為後續 Doctors 會透過外鍵參照它。

Departments 建表 SQL

CREATE TABLE departments (
    department_id NUMBER(12,0) NOT NULL,
    department_name VARCHAR2(100 CHAR) NOT NULL,
    CONSTRAINT pk_departments PRIMARY KEY (department_id),
    CONSTRAINT uq_departments_name UNIQUE (department_name),
    CONSTRAINT ck_departments_id CHECK (department_id > 0),
    CONSTRAINT ck_departments_name CHECK (TRIM(department_name) IS NOT NULL)
);

這次開始實際理解幾個重要的 SQL 設定。

SQL 用途
CREATE TABLE 建立資料表
NUMBER(12,0) 儲存最多 12 位精度、沒有小數位的數值
VARCHAR2(100 CHAR) 以字元長度語意儲存文字
NOT NULL 欄位不可沒有值
PRIMARY KEY 設定唯一識別資料的主鍵
UNIQUE 防止科別名稱重複
CHECK 限制資料必須符合指定條件

其中:

CHECK (department_id > 0)

可以避免使用 0 或負數作為科別 ID。

而:

CHECK (TRIM(department_name) IS NOT NULL)

則用來避免科別名稱只有一般空白。

建表成功後,再透過 user_tables、user_tab_columns 與 user_constraints 檢查資料表的欄位及限制。

這次也發現,Oracle 可能會為 NOT NULL 限制自動產生 SYS_C... 名稱,這並不代表多建立了不需要的資料表。

最後確認 Departments 資料筆數為 0,表示目前只建立了結構,尚未新增內容。


六、建立第二張資料表:Doctors

完成 Departments 後,接著建立 Doctors。

Doctors 是今天最主要的資料表,用來保存醫師基本資料。

Doctors 建表 SQL

CREATE TABLE doctors (
    doctor_id NUMBER(12,0) NOT NULL,
    department_id NUMBER(12,0) NOT NULL,
    doctor_name VARCHAR2(100 CHAR) NOT NULL,
    title VARCHAR2(100 CHAR),
    education VARCHAR2(500 CHAR),
    experience VARCHAR2(500 CHAR),
    illustration_path VARCHAR2(500 CHAR),
    CONSTRAINT pk_doctors PRIMARY KEY (doctor_id),
    CONSTRAINT fk_doctors_department FOREIGN KEY (department_id)
        REFERENCES departments (department_id),
    CONSTRAINT ck_doctors_id CHECK (doctor_id > 0),
    CONSTRAINT ck_doctors_name CHECK (TRIM(doctor_name) IS NOT NULL)
);

這次最重要的是外鍵。

FOREIGN KEY (department_id)
    REFERENCES departments (department_id)

它表示 Doctors 中的 department_id 必須對應 Departments 中已存在的科別 ID。

例如:

醫師 ID 醫師姓名 department_id
1 陳測試甲 1
2 林測試乙 1
3 陳測試丙 2

Doctors 只需要保存科別 ID,不必重複保存科別名稱。

之後查詢時,再透過 JOIN 取得科別名稱即可。

這次也注意到,Doctors 的 doctor_name 沒有設定 UNIQUE,因為不同醫師有可能同名。

完成建表後,我另外查詢外鍵資訊,確認:

DOCTORS.DEPARTMENT_ID
          ↓
DEPARTMENTS.DEPARTMENT_ID

確實已建立正確的關聯。

這也是第一次在 Oracle 中實際建立跨資料表的外鍵關係。


七、建立第三張資料表:DoctorSpecialties

最後建立 DoctorSpecialties,用來保存每位醫師的多筆專長。

DoctorSpecialties 建表 SQL

CREATE TABLE doctor_specialties (
    doctor_id NUMBER(12,0) NOT NULL,
    specialty_name VARCHAR2(100 CHAR) NOT NULL,
    CONSTRAINT pk_doctor_specialties PRIMARY KEY (doctor_id, specialty_name),
    CONSTRAINT fk_specialties_doctor FOREIGN KEY (doctor_id)
        REFERENCES doctors (doctor_id),
    CONSTRAINT ck_specialties_name CHECK (TRIM(specialty_name) IS NOT NULL)
);

這張表只有兩個欄位,但使用了複合主鍵。

PRIMARY KEY (doctor_id, specialty_name)

代表一筆專長紀錄,是由醫師 ID 與專長名稱共同識別。

例如:

doctor_id specialty_name
1 一般內科-測試
1 健康諮詢-測試
2 一般內科-測試

醫師 1 可以有兩種不同專長,而醫師 1 與醫師 2 也可以擁有相同的專長。

但是同一位醫師不能重複新增完全相同的專長名稱。

完成建表後,我也查詢 Oracle 的限制資訊,確認複合主鍵包含:

DOCTOR_ID         POSITION 1
SPECIALTY_NAME    POSITION 2

外鍵則正確對應:

DOCTOR_SPECIALTIES.DOCTOR_ID
               ↓
DOCTORS.DOCTOR_ID

至此,三張醫師查詢相關資料表已成功建立。

建表結果

資料表 欄位數 狀態
Departments 2 建立成功
Doctors 7 建立成功
DoctorSpecialties 2 建立成功
合計 11 完成

SQL Developer 中建立完成的三張 Oracle 資料表。

https://ithelp.ithome.com.tw/upload/images/20261030/20178845JQi3Mfd4kC.png


八、加入虛構測試資料

完成資料表後,接著準備 API 查詢需要的測試資料。

因為目前還沒有正式醫院資料,也不應該使用真實病人或醫師的個人資料,因此這次全部使用虛構資訊。

新增順序為:

Departments
    ↓
Doctors
    ↓
DoctorSpecialties

原因是外鍵的參照資料必須先存在。

1. 新增三個科別

INSERT INTO departments (department_id, department_name)
VALUES (1, '測試內科');

INSERT INTO departments (department_id, department_name)
VALUES (2, '測試外科');

INSERT INTO departments (department_id, department_name)
VALUES (3, '測試兒科');

執行後先使用 SELECT 驗證三筆資料,再進行 COMMIT。

2. 新增五位虛構醫師

本次安排:

醫師 ID 姓名 科別 職稱
1 陳測試甲 測試內科 測試主治醫師
2 林測試乙 測試內科 測試科主任
3 陳測試丙 測試外科 測試主治醫師
4 王測試丁 測試外科 NULL
5 李測試戊 測試兒科 測試主治醫師

其中王測試丁的職稱、學歷、經歷及圖片路徑刻意設成 NULL。

因為未來可能遇到資料尚未補齊的情況,希望藉由這筆資料測試 API 能否正常處理空值。

3. 新增八筆醫師專長

這次的專長安排如下:

醫師 專長數
陳測試甲 2
林測試乙 3
陳測試丙 1
王測試丁 0
李測試戊 2
合計 8

王測試丁刻意沒有專長。

這樣可以測試後續查詢是否會因為醫師沒有對應的專長紀錄,就把他排除在列表之外。

4. 使用 COMMIT 保存資料

每一批 INSERT 完成後,我先執行 SELECT,確認資料內容與筆數正確,再執行:

COMMIT;

這次也實際理解了 COMMIT 的用途。

INSERT 是資料修改操作,在尚未提交的情況下,其他資料庫連線通常還看不到這些修改。

完成 COMMIT 後,資料才正式保留下來。

最後確認三張表:

資料表 實際資料筆數
Departments 3
Doctors 5
DoctorSpecialties 8

至此,醫師查詢 API 需要的基本測試資料已準備完成。


九、第一次使用 JOIN 查詢醫師與科別

資料庫準備完成後,接著練習如何查詢多張資料表。

因為 Doctors 保存的是 department_id,如果要顯示科別名稱,就需要透過 Departments 取得。

這次使用 SQL JOIN:

SELECT d.doctor_id,
       d.doctor_name,
       d.department_id,
       dep.department_name
FROM doctors d
JOIN departments dep
    ON dep.department_id = d.department_id
ORDER BY d.doctor_id;

其中:

JOIN departments dep
    ON dep.department_id = d.department_id

表示將兩張表中 department_id 相同的資料連接起來。

查詢後可以得到醫師姓名與所屬科別名稱。

這讓我理解到:資料庫不需要在每一張表重複保存相同資訊,而是可以利用主鍵、外鍵及 JOIN 取得相關資料。


十、認識 LEFT JOIN:沒有專長的醫師也要顯示

接著遇到一個很重要的問題。

如果直接使用 JOIN 連接 Doctors 和 DoctorSpecialties,沒有專長資料的王測試丁可能不會出現在結果中。

但醫師是否具有專長紀錄,不應決定他能不能出現在醫師列表。

因此這次使用:

SELECT d.doctor_id,
       d.doctor_name,
       COUNT(s.specialty_name) AS specialty_count
FROM doctors d
LEFT JOIN doctor_specialties s
    ON s.doctor_id = d.doctor_id
GROUP BY d.doctor_id, d.doctor_name
ORDER BY d.doctor_id;

LEFT JOIN 的用途

LEFT JOIN 會保留左側資料表中的所有符合其他查詢條件的資料,即使右側沒有對應紀錄。

在這裡:

  • 左側是 Doctors。
  • 右側是 DoctorSpecialties。

因此即使某位醫師沒有專長,也仍然會被保留下來。

為什麼使用 COUNT(s.specialty_name)?

因為 LEFT JOIN 對於沒有專長的醫師,仍可能產生一列右側欄位為 NULL 的結果。

如果使用:

COUNT(*)

就會把那一列也計算進去。

但使用:

COUNT(s.specialty_name)

可以忽略 NULL,正確計算實際的專長數量。

實際查詢結果

醫師 專長數量
陳測試甲 2
林測試乙 3
陳測試丙 1
王測試丁 0
李測試戊 2

這次學到,SQL 的 JOIN 類型會直接影響 API 最後能取得哪些資料。

使用 LEFT JOIN 查詢五位醫師的專長數量,包含沒有專長的醫師。

https://ithelp.ithome.com.tw/upload/images/20261030/20178845088TJfAGQl.png


十一、規劃 ASP.NET Core 的醫師查詢架構

完成資料庫後,接著開始建立真正的醫師查詢 API。

Day 21 已經建立 HelloController,但當時沒有需要讀取業務資料。

這次因為要從 Oracle 查詢醫師資訊,因此需要進一步區分程式碼的責任。

目前採用:

HTTP Request
      ↓
DoctorsController
      ↓
DoctorRepository
      ↓
Oracle Database
      ↓
DoctorListItemDto / DoctorDetailDto
      ↓
HTTP Response JSON

這次主要使用四個類別:

類別 負責工作
DoctorsController 接收 HTTP 請求、驗證參數、回傳狀態碼
DoctorRepository 連接 Oracle、執行 SQL、整理查詢結果
DoctorListItemDto 定義醫師列表的 JSON 欄位
DoctorDetailDto 定義醫師詳細資料的 JSON 欄位

目前沒有另外加入 Service 層或 Entity Framework Core。

因為這次主要是讀取資料,希望先理解 Controller、Repository 與 DTO 的基本分工,再逐步擴充。


十二、認識 DTO:決定 API 要回傳哪些欄位

DTO 是 Data Transfer Object,也就是資料傳輸物件。

這次使用 DTO 定義 API 要提供給前端的資料格式。

DoctorListItemDto

實際建立:

namespace SmartCare.Api.Dtos;

public sealed class DoctorListItemDto
{
    public required long Id { get; init; }
    public required string Name { get; init; }
    public required long DepartmentId { get; init; }
    public required string DepartmentName { get; init; }
    public string? Title { get; init; }
    public string? IllustrationPath { get; init; }
    public List<string> Specialties { get; init; } = [];
}

這次學習到幾個 C# 語法:

required

表示建立 DTO 物件時,必須指定這個屬性。

init

表示屬性一般在物件初始化時設定,避免物件建立後任意重新指定。

string?

表示欄位允許 null。

例如醫師沒有職稱或圖片路徑時,就可以回傳 null。

List

表示一個字串清單,可以保存多個專長。

public List<string> Specialties { get; init; } = [];

即使沒有專長,也會回傳空陣列,而不是要求前端自行猜測資料是否存在。

DTO 的另一個好處,是不需要直接將資料庫查詢物件提供給前端。


十三、建立 DoctorRepository 查詢 Oracle

接著建立 DoctorRepository,負責真正讀取 Oracle 資料。

醫師列表查詢需要結合三張資料表:

SELECT d.doctor_id,
       d.doctor_name,
       d.department_id,
       dep.department_name,
       d.title,
       d.illustration_path,
       s.specialty_name
FROM doctors d
JOIN departments dep
    ON dep.department_id = d.department_id
LEFT JOIN doctor_specialties s
    ON s.doctor_id = d.doctor_id
ORDER BY d.doctor_id, s.specialty_name;

這裡同時使用 JOIN 與 LEFT JOIN:

  • JOIN:取得醫師所屬科別名稱。
  • LEFT JOIN:取得醫師專長,並保留沒有專長的醫師。

使用 OracleConnection 與 OracleCommand

這次沿用 Day 23 學過的 Oracle .NET 驅動程式。

查詢核心:

await using var connection =
    new OracleConnection(settings.ConnectionString);

await connection.OpenAsync(timeout.Token);

await using var command = new OracleCommand(sql, connection)
{
    BindByName = true,
    CommandTimeout = 5
};

await using var reader =
    await command.ExecuteReaderAsync(timeout.Token);

其中:

  • OracleConnection:建立資料庫連線。
  • OpenAsync:非同步開啟連線。
  • OracleCommand:執行 SQL。
  • ExecuteReaderAsync:取得查詢結果。
  • await using:在使用結束時釋放資源。

一位醫師為什麼可能有多列結果?

因為 DoctorSpecialties 是一對多關係。

例如陳測試甲有兩個專長,SQL JOIN 後可能取得兩列:

陳測試甲 → 一般內科-測試
陳測試甲 → 健康諮詢-測試

但 API 不應該因此回傳兩位陳測試甲。

因此 Repository 使用 Dictionary,以醫師 ID 作為識別,將相同醫師的資料合併成同一個 DTO,再將不同專長加入 Specialties 陣列。

這次也學到,SQL 查詢結果與最終 API JSON 結構不一定完全相同,中間需要由 C# 負責整理。


十四、建立 DoctorsController 與依賴注入

完成 Repository 後,接著建立 DoctorsController。

Controller 不負責直接寫 SQL,而是透過 Repository 取得查詢結果。

醫師列表 Action 的核心:

[HttpGet]
public async Task<IActionResult> GetAll(
    CancellationToken cancellationToken)
{
    try
    {
        var doctors =
            await _repository.GetAllAsync(cancellationToken);

        return Ok(new { data = doctors });
    }

    // 完整程式另外包含例外處理。
}

上面是最初醫師列表功能的程式碼節錄,後續加入搜尋與篩選後,Action 也增加了查詢參數。

其中:

return Ok(new { data = doctors });

會產生 HTTP 200 回應,並讓 ASP.NET Core 將物件序列化成 JSON。

認識依賴注入

另外,在 Program.cs 中加入:

using SmartCare.Api.Data;

並註冊:

builder.Services.AddScoped<DoctorRepository>();

這是 ASP.NET Core 的依賴注入(Dependency Injection)。

讓 DoctorsController 可以透過建構子取得 DoctorRepository,不需要自己建立 Repository 物件。

這次開始了解,後端程式不只是將所有內容放在同一個 Controller,而是依照不同責任整理程式碼。

DoctorsController、DoctorRepository 與 DTO 的程式碼結構。

https://ithelp.ithome.com.tw/upload/images/20261030/201788456kkIgfKe35.png


十五、第一支醫師列表 API:GET /api/doctors

完成 Controller 與 Repository 後,第一次實際測試醫師列表 API。

API:

GET /api/doctors

這支 API 會從 Oracle 查詢 Doctors、Departments 與 DoctorSpecialties,並將結果整理成 JSON。

實際測試結果

  • HTTP 狀態碼:200。
  • 回傳五位虛構醫師。
  • 醫師 ID、姓名與科別符合資料庫測試資料。
  • 專長數量為 2、3、1、0、2。
  • 沒有專長的王測試丁仍出現在結果中。
  • 王測試丁的選填欄位可以正常回傳 null。

例如王測試丁的列表資料:

{
  "id": 4,
  "name": "王測試丁",
  "departmentId": 2,
  "departmentName": "測試外科",
  "title": null,
  "illustrationPath": null,
  "specialties": []
}

這次最重要的成果,是醫師資料已經真正從 Oracle 查出,而不是寫在 Controller 裡的固定資料。


十六、加入醫師姓名關鍵字搜尋

完成醫師列表後,接著加入搜尋功能。

原本的 API:

GET /api/doctors

新增可選的 keyword 參數:

GET /api/doctors?keyword=陳

這次沒有另外建立新的搜尋路由,而是沿用同一支 API。

Controller 接收:

[HttpGet]
public async Task<IActionResult> GetAll(
    [FromQuery] string? keyword,
    CancellationToken cancellationToken)

[FromQuery] 表示從 URL 的 Query String 取得參數。

string? 表示可以不提供 keyword。

使用 OracleParameter 避免直接拼接 SQL

這次學習到 SQL Injection 的基本概念。

如果直接將使用者輸入拼接進 SQL,可能讓輸入內容改變原本的查詢結構。

因此改用參數化 SQL:

WHERE (:keyword IS NULL OR INSTR(d.doctor_name, :keyword) > 0)

搭配:

command.Parameters.Add(
    new OracleParameter("keyword", OracleDbType.Varchar2, 100)
    {
        Value = (object?)keyword ?? DBNull.Value
    });

:keyword 是 SQL 參數佔位符。

INSTR 則用來檢查醫師姓名是否包含關鍵字。

這次選擇 INSTR,而不是直接使用 LIKE,是因為 %、_ 在 LIKE 中具有特殊意義。

使用 INSTR 時,可以將這些字元當成一般搜尋文字。

搜尋的實際結果

keyword 實際結果
未提供 五位醫師
陳 陳測試甲、陳測試丙
王 王測試丁
不存在 空陣列

例如:

GET /api/doctors?keyword=不存在

實際回傳:

{
  "data": []
}

因為這是查詢成功但沒有符合的資料,因此回傳 200,而不是 404。

搜尋輸入驗證

這次也加入驗證規則:

  • 未提供或一般空白 keyword:查詢全部醫師。
  • 頭尾空白:去除後搜尋。
  • 過長輸入:回傳 400。
  • 控制字元:回傳 400。
  • 重複 keyword:回傳 400。
  • 特殊字元:當成一般文字搜尋。

這次姓名搜尋共完成 17 個測試案例,且原有 API 仍能正常運作。


十七、加入科別篩選與組合搜尋

完成姓名搜尋後,接著加入科別篩選。

目前 API 已經支援:

GET /api/doctors?departmentId=1

以及:

GET /api/doctors?keyword=陳&departmentId=1

這次學習如何讓一支 API 同時接收多個 Query String 參數。

Controller 修改為:

[HttpGet]
public async Task<IActionResult> GetAll(
    [FromQuery] string? keyword,
    [FromQuery] long? departmentId,
    CancellationToken cancellationToken)

其中 long? 表示 departmentId 可以是整數,也可以沒有提供。

使用 AND 組合條件

Repository 的 SQL:

WHERE (:keyword IS NULL OR INSTR(d.doctor_name, :keyword) > 0)
  AND (:departmentId IS NULL OR d.department_id = :departmentId)

這次主要理解了 AND 的用途。

當兩個參數都有提供時,醫師必須同時符合姓名與科別條件。

如果只提供其中一個,就只使用該條件。

Oracle 數字參數

科別 ID 使用:

command.Parameters.Add(
    new OracleParameter("departmentId", OracleDbType.Int64)
    {
        Value = (object?)departmentId ?? DBNull.Value
    });

這樣就能將數字參數與 SQL 分開處理,而不是直接拼接到查詢字串中。

實際測試

查詢條件 實際結果
departmentId=1 醫師 1、2
departmentId=2 醫師 3、4
departmentId=3 醫師 5
keyword=陳&departmentId=1 醫師 1
keyword=陳&departmentId=2 醫師 3
keyword=王&departmentId=2 醫師 4
keyword=陳&departmentId=3 空陣列

另外,也針對無效的科別 ID 進行測試。

例如:

  • 0。
  • 負數。
  • 非數字。
  • 超出 long 範圍。
  • 重複提供參數。
  • 小數。

這些不合法輸入會回傳 400。

如果是合法但不存在的科別 ID,例如 999,則回傳 200 與空陣列。

完成這個階段後,包含原有 17 個姓名搜尋案例、新增 23 個案例及 2 個既有 API,共 42 個 HTTP 檢查全部通過。

姓名與科別組合搜尋 API 成功回傳符合條件的醫師。

https://ithelp.ithome.com.tw/upload/images/20261030/20178845qoHkW9dLMV.png


十八、建立醫師詳細資料 API

完成列表、搜尋與科別篩選後,最後實作醫師詳細資料 API。

這次的目標是讓使用者透過醫師 ID,取得指定醫師的完整資訊。

例如:

GET /api/doctors/1

建立 DoctorDetailDto

詳細資料比列表多出學歷及經歷,因此另外建立:

namespace SmartCare.Api.Dtos;

public sealed class DoctorDetailDto
{
    public required long Id { get; init; }
    public required string Name { get; init; }
    public required long DepartmentId { get; init; }
    public required string DepartmentName { get; init; }
    public string? Title { get; init; }
    public string? Education { get; init; }
    public string? Experience { get; init; }
    public string? IllustrationPath { get; init; }
    public List<string> Specialties { get; init; } = [];
}

列表 DTO 與詳細 DTO 的用途不同。

列表主要提供搜尋結果卡片需要的資訊;詳細 DTO 則提供更完整的學經歷資料。

新增詳細查詢路由

Controller 加入:

[HttpGet("{id}")]
public async Task<IActionResult> GetById(
    [FromRoute] long id,
    CancellationToken cancellationToken)

其中:

[HttpGet("{id}")]

表示網址中會包含醫師 ID。

例如:

/api/doctors/1

而:

[FromRoute] long id

表示從網址路徑取得 ID,再交給 Repository 查詢。

Repository 使用參數化 SQL

詳細查詢的條件為:

WHERE d.doctor_id = :id

搭配:

command.Parameters.Add(
    new OracleParameter("id", OracleDbType.Int64)
    {
        Value = id
    });

這次同樣沒有將使用者提供的 ID 直接拼接進 SQL。

Repository 透過 JOIN 取得科別,再使用 LEFT JOIN 查詢專長。

如果某位醫師有多個專長,就將查詢列合併成同一個 DoctorDetailDto。

處理找不到醫師的情況

這次也開始區分列表查詢與詳細查詢的回傳行為。

列表搜尋不到符合資料時,回傳:

{
  "data": []
}

但如果要求的是指定醫師:

GET /api/doctors/999

而這位醫師不存在,就會回傳 HTTP 404。

因為這次要取得的是某個特定資源,而不是一個可以為空的查詢集合。


十九、醫師詳細資料 API 的實際測試

完成詳細查詢後,使用實際 Oracle 測試資料進行驗證。

1. 查詢醫師 1

GET /api/doctors/1

實際回傳 HTTP 200:

{
  "data": {
    "id": 1,
    "name": "陳測試甲",
    "departmentId": 1,
    "departmentName": "測試內科",
    "title": "測試主治醫師",
    "education": "虛構醫學院甲-測試學歷",
    "experience": "測試經歷甲,非真實資歷",
    "illustrationPath": "/image/doctor-directory/portrait-01.png",
    "specialties": [
      "一般內科-測試",
      "健康諮詢-測試"
    ]
  }
}

可以看到除了列表原有的資訊,也包含學歷與經歷。

2. 查詢沒有專長的醫師

接著查詢:

GET /api/doctors/4

實際回傳:

{
  "data": {
    "id": 4,
    "name": "王測試丁",
    "departmentId": 2,
    "departmentName": "測試外科",
    "title": null,
    "education": null,
    "experience": null,
    "illustrationPath": null,
    "specialties": []
  }
}

這次確認 API 可以正確處理:

  • 職稱為 null。
  • 學歷為 null。
  • 經歷為 null。
  • 圖片路徑為 null。
  • 專長是空陣列。

這也是前面刻意建立不完整測試資料的原因。

3. 不存在的醫師

GET /api/doctors/999

實測回傳 HTTP 404,並使用 ProblemDetails 表示找不到指定醫師。

4. 不合法的醫師 ID

這次也測試:

  • ID 為 0。
  • 負數 ID。
  • 非數字。
  • 小數。
  • 超出 long 範圍。
  • 格式不合法的字串。

這些不合法的輸入會回傳 400。

5. 資料庫設定不足

另外測試缺少連線設定的情境,確認 API 會回傳 503,而不是直接公開 Oracle 密碼、完整連線字串或例外內容。

不過,這次尚未模擬真正的 Oracle 服務中斷、資料庫停機或網路逾時。

查詢單一醫師完整 JSON,包含學歷、經歷及專長。

https://ithelp.ithome.com.tw/upload/images/20261030/201788458xKb7zuClC.png

不存在的醫師 ID 回傳 404 ProblemDetails。

https://ithelp.ithome.com.tw/upload/images/20261030/20178845XCrXyTyn5e.png


二十、完整測試結果

本次 Day 24 從資料庫建立、醫師列表、姓名搜尋、科別篩選,到詳細資料查詢,全部依照不同階段逐步實作與測試。

實際完成的 API

API 功能 結果
GET /api/doctors 取得所有醫師 成功
GET /api/doctors?keyword=陳 姓名搜尋 成功
GET /api/doctors?departmentId=1 科別篩選 成功
GET /api/doctors?keyword=陳&departmentId=1 組合搜尋 成功
GET /api/doctors/1 醫師詳細資料 成功

HTTP 狀態碼處理

HTTP 狀態碼 本次用途
200 查詢成功,包含空列表
400 搜尋參數或醫師 ID 不合法
404 指定醫師不存在
503 缺少資料庫設定或無法完成查詢

最終測試

檢查項目 實際結果
原有搜尋與篩選檢查 42 個通過
新增詳細資料檢查 19 個通過
HTTP 檢查總數 61 個全部通過
dotnet build 成功
編譯警告 0
編譯錯誤 0
GET /api/hello 200
Oracle 診斷 API 200
Angular 原有頁面 未修改
Oracle 測試資料 後端 API 階段未修改

這次也比對了 134 個受保護檔案,確認既有功能沒有受到不必要的修改。

需要注意的是,這些 HTTP 測試是針對目前已完成的醫師查詢功能,不代表會員登入、預約掛號或完整智慧醫院系統都已完成測試。


二十一、這次開發遇到的問題與解決方式

1. 原本只有資料庫連線,沒有可以查詢的業務資料

Day 23 已經可以執行 SELECT 1 FROM DUAL,但尚未建立正式資料表。

因此 Day 24 必須先回到資料庫設計,確認 Day 22 的表結構,再開始建表與 INSERT。

這讓我發現,實作 API 之前,必須先準備正確的資料來源。

2. SMARTCARE_DEV 沒有建表權限

一開始發現開發帳號只有 CREATE SESSION。

後來才知道,資料庫使用者能夠登入,不代表具有建立資料表的權限,也不一定具有可使用的 Tablespace 配額。

因此這次由管理員設定 CREATE TABLE 與有限配額,再使用 SMARTCARE_DEV 建立自己的資料表。

3. 建表順序不能隨意安排

因為 Doctors 參照 Departments,DoctorSpecialties 又參照 Doctors,所以必須依照外鍵關係建立。

這次採用逐張建立、逐張驗證,避免一次執行全部 SQL 後才發現問題。

4. SQL JOIN 會讓一位醫師出現多列

一位醫師可能有多個專長,因此 JOIN 後不一定是一位醫師對應一列。

後來在 Repository 中透過 Dictionary,將相同醫師 ID 的資料合併成一個 DTO,再把專長放入 List。

這次才更清楚理解,資料庫查詢結果需要轉換成適合前端使用的 JSON 結構。

5. 沒有專長的醫師不能被排除

如果直接使用一般 INNER JOIN 連接專長表,沒有專長紀錄的醫師可能不會出現在結果中。

因此使用 LEFT JOIN,讓沒有專長的醫師也可以正常查詢。

6. 使用者輸入需要驗證

加入姓名搜尋與科別篩選後,需要處理空白、重複參數、非法數字、長度限制與特殊字元。

這次透過 Controller 的驗證與 OracleParameter 的參數化查詢,將輸入檢查和資料庫查詢分開處理。

7. 本機測試腳本問題

本次醫師列表測試腳本曾遇到本機通訊權限與 UTF-8 解碼問題。

後續經過修正,才完成測試。

這提醒我,測試失敗不一定表示 API 或 SQL 有錯,也可能是測試工具、文字編碼或執行環境造成。

因此除了解決功能問題,也要確認錯誤實際發生在哪一個環節。


二十二、今日學習與 AI 協助

今日學習

這次主要學習:

Oracle Database

  • CREATE TABLE。
  • PRIMARY KEY。
  • FOREIGN KEY。
  • 複合主鍵。
  • NOT NULL、UNIQUE、CHECK。
  • CREATE TABLE 權限與 Tablespace 配額。
  • INSERT INTO。
  • COMMIT 與 ROLLBACK。
  • JOIN、LEFT JOIN。
  • GROUP BY 與 COUNT。
  • Oracle SQL 參數。

ASP.NET Core 與 C#

  • Controller。
  • Repository。
  • DTO。
  • Dependency Injection。
  • OracleConnection。
  • OracleCommand。
  • OracleDataReader。
  • 非同步資料庫查詢。
  • C# required、init、string?。
  • Dictionary 與 List。
  • [FromQuery]、[FromRoute]。
  • Query String。
  • HTTP 200、400、404、503。
  • ProblemDetails。
  • API 測試與回歸測試。

AI 協助

本次使用 Codex 協助:

  • 檢查 Day 22 的原始資料庫設計。
  • 確認 Oracle 開發帳號權限與配額。
  • 準備三張資料表 DDL。
  • 規劃虛構醫師測試資料。
  • 提供 SQL Developer 的分階段操作方式。
  • 檢查主鍵、外鍵及欄位限制。
  • 規劃 DoctorsController、DoctorRepository 與 DTO 的結構。
  • 實作 Oracle 醫師查詢。
  • 加入姓名搜尋及參數驗證。
  • 加入科別篩選與組合搜尋。
  • 實作醫師詳細資料。
  • 建立自動化測試腳本。
  • 整理測試紀錄及程式碼說明。

這次的資料庫建表與 INSERT,主要由我在 SQL Developer 中手動執行,再根據實際結果確認是否可以進入下一步。

後端則透過 Codex 協助修改與測試,但每個階段都有整理程式碼用途,讓我可以逐步了解實作方式,而不是直接完成後就進入下一項功能。


二十三、今日成果與下一步

今日完成

這次已經完成:

  1. 在 Oracle 建立三張醫師相關資料表。
  2. 建立主鍵、外鍵、複合主鍵與資料限制。
  3. 新增 3 筆科別、5 筆虛構醫師、8 筆專長。
  4. 使用 JOIN 取得醫師所屬科別。
  5. 使用 LEFT JOIN 保留沒有專長的醫師。
  6. 建立 DoctorsController。
  7. 建立 DoctorRepository。
  8. 建立 DoctorListItemDto。
  9. 建立 DoctorDetailDto。
  10. 完成醫師列表 API。
  11. 完成姓名關鍵字搜尋。
  12. 完成科別篩選。
  13. 完成姓名與科別組合搜尋。
  14. 完成醫師詳細資料 API。
  15. 驗證空結果、NULL、400、404、503。
  16. 完成 61 個 HTTP 檢查。
  17. 確認原本 Hello API 與 Oracle 診斷 API 沒有受到影響。

目前尚未完成

雖然醫師查詢後端已經完成,但目前 Angular 醫師列表與詳細頁面仍然使用前端 Demo 資料。

因此還沒有實現真正的前後端整合。

另外,Day 22 規劃的其他四張資料表,以及登入、排班和預約掛號的後端功能,也仍屬於後續工作。

下一步:Day 25|Angular 串接醫師查詢 API

接下來希望讓 Angular 透過 HttpClient 呼叫 ASP.NET Core API,取得 Oracle Database 中的醫師資料。

預計學習:

  • Angular HttpClient。
  • Service 的建立與使用。
  • API Request 與 Response。
  • Observable 的基本概念。
  • Loading 與錯誤狀態。
  • 前端 Demo 資料與後端 DTO 的欄位轉換。
  • Angular 與 ASP.NET Core 的跨來源請求設定。
  • 醫師列表與詳細頁面串接。

希望 Day 25 可以讓原本只顯示 Demo 資料的醫師查詢頁面,開始真正取得 Oracle 中的資料。

這次最大的收穫,是第一次完成一個從 Oracle 資料表、SQL 查詢、C# Repository、ASP.NET Core Controller,到 HTTP JSON 回傳 的完整後端資料流程。

也讓我開始理解,真正的網站功能不只是做出畫面,而是需要考慮資料結構、資料關聯、輸入驗證、錯誤處理,以及不同程式層之間的責任分工。



上一篇
Day 23|ASP.NET Core 連接 Oracle
下一篇
Day 25|Angular 串接醫師 API
系列文
從 Figma 到 Angular:打造智慧醫院預約掛號系統的 30 天實作之旅 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言